﻿using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Data.Linq.Mapping;
using System.Threading.Tasks;
using System.Reflection;

using LeanEngine.Entity;
using LeanEngine.Extensions;
using LeanEngine.Collections;
using LeanEngine.Data;
using LeanEngine.Validation;
using LeanEngine.Data.Linq.Mapping;
using LeanEngine.Reflection;

namespace LeanEngine.Business
{
    /// <summary>
    /// Represents a set of business method for all basic operations.
    /// </summary>
    public static class EntityCrudExtensions
    {
        #region Declaration Members

        private static Func<PropertyInfo, AssociationTableAttribute> _getAssociation = (property) => property.GetCustomAttributes(typeof(AssociationTableAttribute), false).FirstOrDefault() as AssociationTableAttribute;
        private static Func<PropertyInfo, object, object> _getValue = (property, paramValue) => ReflectionCaches.PropertyAccessorCache.GetItem(property).GetValue(paramValue);
        private static Func<Type, PropertyInfo> _getDeletedList = (listType) => listType.GetProperty("DeletedList");
        private static Func<Type, PropertyInfo> _getInsertedList = (listType) => listType.GetProperty("InsertedList");

        #endregion

        #region List<IDataTransferObject> Extension Members

        /// <summary>
        /// Validates whether all data are valid in the entire list.
        /// </summary>
        /// <param name="valueList">The list of instances of the <typeparam name="T" /> for validate.</param>
        /// <exception cref="System.ArgumentNullException"></exception>
        /// <exception cref="LeanEngine.Exception.BusinessException"></exception>
        public static bool IsValid<T>(this ICollection<T> valueList)
            where T : PlainEntity
        {
            foreach (var value in valueList)
            {
                if (!value.IsValid())
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Creates a massive amount of new rows in the database using a bulk insert technique.
        /// </summary>
        /// <param name="valuesList">The data transfer object instance that holds all data to insert in the database.</param>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="System.Data.DataException"></exception>
        /// <exception cref="LeanEngine.BusinessException"></exception>
        public static void BulkInsert<T>(this ICollection<T> valuesList)
            where T : PlainEntity
        {
            if (valuesList.IsNull()) throw new ArgumentNullException("valuesList");
            
            var list = valuesList.ToList();
            if (list.IsNotNullOrEmpty())
            {
                using (var cmd = new DataCommand())
                {
                    cmd.ExecuteBulkInsert(list);
                };
            }
        }

        /// <summary>
        /// Creates a list of new rows in the database using a parallel technique.
        /// </summary>
        /// <param name="valuesList">The data transfer object instance that holds all data to insert in the database.</param>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="System.Data.DataException"></exception>
        /// <exception cref="LeanEngine.BusinessException"></exception>
        public static void ParallelInsert<T>(this ICollection<T> valuesList)
            where T : PlainEntity
        {
            if (valuesList.IsNull()) throw new ArgumentNullException("valuesList");

            Parallel.ForEach<T>(valuesList, item =>
            {
                item.Insert();
            });
        }

        /// <summary>
        /// Creates a list of new rows in the database.
        /// </summary>
        /// <param name="valuesList">The data transfer object instance that holds all data to insert in the database.</param>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="System.Data.DataException"></exception>
        /// <exception cref="LeanEngine.BusinessException"></exception>
        public static void Insert<T>(this ICollection<T> valuesList)
            where T : PlainEntity
        {
            if (valuesList.IsNull()) throw new ArgumentNullException("valuesList");

            foreach(var item in valuesList)
            {
                item.Insert();
            };
        }

        /// <summary>
        /// Deletes a list of rows from the database using a parallel technique.
        /// </summary>
        /// <param name="valuesList">The data transfer object instance that holds the primary key values of the row to delete.</param>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="System.Data.DataException"></exception>
        /// <exception cref="LeanEngine.BusinessException"></exception>
        public static void ParallelDelete<T>(this ICollection<T> valuesList)
            where T : PlainEntity
        {
            if (valuesList.IsNull()) throw new ArgumentNullException("valuesList");

            Parallel.ForEach<T>(valuesList, item =>
            {
                item.Delete();
            });
        }

        /// <summary>
        /// Deletes a list of rows from the database.
        /// </summary>
        /// <param name="valuesList">The data transfer object instance that holds the primary key values of the row to delete.</param>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="System.Data.DataException"></exception>
        /// <exception cref="LeanEngine.BusinessException"></exception>
        public static void Delete<T>(this ICollection<T> valuesList)
            where T : PlainEntity
        {
            if (valuesList.IsNull()) throw new ArgumentNullException("valuesList");

            foreach(var item in valuesList)
            {
                item.Delete();
            }
        }

        #endregion

        #region IDataTransferObject Extension Members

        /// <summary>
        /// Validates whether all data are valid.
        /// </summary>
        /// <param name="value">The instance of the LeanEngine.IDataTransferObject for validate.</param>
        /// <exception cref="System.ArgumentNullException"></exception>
        /// <exception cref="LeanEngine.BusinessException"></exception>
        public static bool IsValid(this PlainEntity value)
        {
            foreach(var property in value.GetType().GetProperties())
            {
                var propertyValue = property.FastGetValue(value);
                if (!ValidationHelper.Validate(propertyValue, ValidatorMetadataCache.GetValidatorList(property)))
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Creates a new row in the database whether all data validations are valid.
        /// </summary>
        /// <param name="value">The data transfer object instance that holds all data to insert in the database.</param>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="System.Data.DataException"></exception>
        /// <exception cref="LeanEngine.BusinessException"></exception>
        public static void Insert(this PlainEntity value) 
        {
            if (value.IsNull())
            {
                throw new ArgumentNullException("value");
            }

            using(var cmd = new CrudCommand())
            {
                ExecuteInsert(cmd, value);
            }
        }

        private static void ExecuteInsert(CrudCommand cmd, PlainEntity value)
        {
            var changeTracker = value as ChangeTrackerEntity;

            cmd.ExecuteInsert(value);

            foreach (var property in value.GetType().GetProperties())
            {
                var associationTable = _getAssociation(property);
                if (associationTable != null && associationTable.AutoInsert)
                {
                    var propertyValue = _getValue(property, value) as IList;

                    if (propertyValue != null)
                    {
                        cmd.ExecuteInsertRelation(associationTable.Name, value, propertyValue);
                        (propertyValue as IChangeTrack).SaveState();
                    }
                }
            }

            if (changeTracker.IsNotNull()) changeTracker.SaveState();
        }

        /// <summary>
        /// Updates a row in the database whether all data validations are valid.
        /// </summary>
        /// <param name="value">The data transfer object instance that holds all data to update in the database.</param>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="System.Data.DataException"></exception>
        /// <exception cref="LeanEngine.BusinessException"></exception>
        public static void Update(this PlainEntity value)
        {
            if (value.IsNull()) throw new ArgumentNullException("value");

            using (var cmd = new CrudCommand())
            {
                cmd.ExecuteUpdate(value);
                UpdateRelation(value, cmd);
            }

            var dtoValue = value as ChangeTrackerEntity;
            if (dtoValue.IsNotNull()) dtoValue.SaveState();
        }

        /// <summary>
        /// Deletes a row int the database according all primary key values of <typeparamref name="T"/> object.
        /// </summary>
        /// <param name="value">The data transfer object instance that holds the primary key values of the row to delete.</param>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="System.Data.DataException"></exception>
        /// <exception cref="LeanEngine.BusinessException"></exception>
        public static void Delete(this PlainEntity value)
        {
            if (value.IsNull()) throw new ArgumentNullException("value");            

            using (var cmd = new CrudCommand())
            {
                foreach (var property in value.GetType().GetProperties())
                {
                    var associationTable = _getAssociation(property);
                    if (associationTable != null && associationTable.AutoDelete)
                    {
                        var propertyValue = _getValue(property, value) as IList;

                        cmd.ExecuteDeleteRelation(associationTable.Name, value, propertyValue);
                        
                        (propertyValue as IChangeTrack).SaveState();
                    }
                }

                cmd.ExecuteDelete(value);
            }
        }

        private static void UpdateRelation(PlainEntity entity, CrudCommand command)
        {
            var changeTracker = entity as ChangeTrackerEntity;
            var metaTable = MetaTableCache.GetItem(entity.GetType());

            foreach (var column in metaTable.ColumnsList)
            {
                var associationTable = _getAssociation(column.PropertyInfo);
                var propertyValue = column.GetValue(entity);

                if (associationTable != null && propertyValue != null)
                {
                    var listType = propertyValue.GetType();

                    var deletedList = _getValue(_getDeletedList(listType), propertyValue) as IList;
                    var insertedList = _getValue(_getInsertedList(listType), propertyValue) as IList;

                    if (associationTable.AutoDelete) command.ExecuteDeleteRelation(associationTable.Name, entity, deletedList);
                    if (associationTable.AutoInsert) command.ExecuteInsertRelation(associationTable.Name, entity, insertedList);

                    (propertyValue as IChangeTrack).SaveState();
                }
            }
        }

        #endregion
    }
}
